/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.gateway.server.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.iwindplus.boot.domain.constant.CommonConstant.NumberConstant;
import com.iwindplus.boot.domain.constant.CommonConstant.RegexConstant;
import com.iwindplus.boot.domain.context.UserContextHolder;
import com.iwindplus.boot.domain.vo.TraceInfoVO;
import com.iwindplus.boot.domain.vo.UserBaseVO;
import com.iwindplus.boot.util.AddressUtil;
import com.iwindplus.boot.util.DatesUtil;
import com.iwindplus.boot.util.UrlUtil;
import com.iwindplus.boot.util.domain.vo.AddressVO;
import com.iwindplus.boot.web.manager.context.ContextTransManager;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant.ServerWebExchangeAttributeConstant;
import com.iwindplus.gateway.server.domain.event.GatewayLogEvent;
import com.iwindplus.gateway.server.domain.property.GatewayProperty;
import com.iwindplus.log.domain.dto.GatewayLogDTO;
import jakarta.annotation.Resource;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchange.Builder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

/**
 * 日志过滤器.
 *
 * @author zengdegui
 * @since 2020/4/15
 */
@Slf4j
@Component
public class LogFilter implements Ordered, GlobalFilter {

    @Resource
    private GatewayProperty property;

    @Resource
    private ApplicationContext applicationContext;

    @Resource
    private DataBufferFactory dataBufferFactory;

    @Resource
    private ContextTransManager contextTransManager;

    @Override
    public int getOrder() {
        return GatewayConstant.FilterConstant.FILTER_LOG_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 开关未启用跳过当前过滤器
        ServerHttpRequest request = exchange.getRequest();
        String requestPath = request.getPath().value();

        final boolean flag = Boolean.FALSE.equals(property.getLog().getEnabled())
            || this.hasIgnoredWhitedFlag(requestPath)
            || UserContextHolder.getContext() == null;
        if (flag) {
            return chain.filter(exchange);
        }

        StopWatch stopWatch = null;
        if (log.isInfoEnabled()) {
            // 统计执行时间任务
            stopWatch = new StopWatch();
            stopWatch.start(LogFilter.class.getSimpleName());
        }

        GatewayLogDTO entity = this.buildGatewayLog(exchange);

        MediaType mediaType = request.getHeaders().getContentType();
        boolean isJsonOrForm = MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)
            || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType);

        return isJsonOrForm
            ? this.writeBodyLog(exchange, chain, entity, stopWatch)
            : this.writeBasicLog(exchange, chain, entity, stopWatch);

    }

    private boolean hasIgnoredWhitedFlag(String requestPath) {
        List<String> ignoredPatterns = property.getLog().getIgnoredApi();
        if (CollUtil.isNotEmpty(ignoredPatterns)) {
            List<String> whiteList = ignoredPatterns.stream().distinct().collect(Collectors.toCollection(ArrayList::new));
            return UrlUtil.isMatchLike(whiteList, requestPath);
        }
        return false;
    }

    private GatewayLogDTO buildGatewayLog(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        final TraceInfoVO traceInfo = this.contextTransManager.getTraceInfo();
        UserBaseVO userInfo = UserContextHolder.getContext();

        long beginMillis = exchange.getAttribute(ServerWebExchangeAttributeConstant.BEGIN_TIME);
        String requestHeaders = exchange.getAttribute(ServerWebExchangeAttributeConstant.REQUEST_HEADERS);

        GatewayLogDTO param = GatewayLogDTO.builder()
            .targetServer(route != null ? route.getId() : null)
            .requestSchema(request.getURI().getScheme())
            .requestPath(request.getPath().pathWithinApplication().value())
            .requestType(request.getMethod().name())
            .requestHeaders(requestHeaders)
            .requestBeginTime(DatesUtil.parseDate(beginMillis, DatePattern.NORM_DATETIME_MS_PATTERN))
            .orgId(userInfo.getOrgId())
            .userId(userInfo.getUserId())
            .createdId(userInfo.getUserId())
            .createdBy(userInfo.getRealName())
            .modifiedBy(userInfo.getRealName())
            .modifiedId(userInfo.getUserId())
            .build();

        if (property.getLog().getEnabledRequestParam()) {
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            if (CollUtil.isNotEmpty(queryParams)) {
                param.setQueryParams(URLUtil.buildQuery(queryParams, Charset.defaultCharset()));
            }
        }

        buildSystemInfo(param, request);
        buildLocation(param, traceInfo);
        return param;
    }

    private void buildSystemInfo(GatewayLogDTO entity, ServerHttpRequest request) {
        String userAgentStr = request.getHeaders().getFirst(HttpHeaders.USER_AGENT);
        if (CharSequenceUtil.isNotBlank(userAgentStr)) {
            UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
            entity.setPlatformName(userAgent != null && userAgent.getPlatform() != null ? userAgent.getPlatform().getName() : null);
            entity.setOsName(userAgent != null && userAgent.getOs() != null ? userAgent.getOs().getName() : null);
            entity.setBrowserName(userAgent != null && userAgent.getBrowser() != null ? userAgent.getBrowser().getName() : null);
        }
    }

    private void buildLocation(GatewayLogDTO entity, TraceInfoVO traceInfo) {
        if (traceInfo != null) {
            entity.setTraceId(traceInfo.getTraceId());
            entity.setIp(traceInfo.getRealIp());
        }

        if (CharSequenceUtil.isNotBlank(entity.getIp())) {
            AddressVO result = AddressUtil.getAddress(entity.getIp());
            entity.setProvince(result != null ? result.getProvince() : null);
            entity.setCity(result != null ? result.getCity() : null);
        }
    }

    private Mono<Void> writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLogDTO entity, StopWatch stopWatch) {
        final Builder builder = exchange.mutate();

        final ServerHttpRequestDecorator decoratedRequest = this.requestDecorate(exchange, entity);
        if (ObjectUtil.isNotEmpty(decoratedRequest)) {
            builder.request(decoratedRequest);
        }

        return this.writeBasicLog(builder.build(), chain, entity, stopWatch);
    }

    private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLogDTO entity, StopWatch stopWatch) {
        final Builder builder = exchange.mutate();

        ServerHttpResponseDecorator decoratedResponse = this.recordResponseLog(exchange, entity);
        if (ObjectUtil.isNotEmpty(decoratedResponse)) {
            builder.response(decoratedResponse);
        }

        return chain.filter(builder.build())
            .doOnTerminate(() -> {
                this.buildGatewayLogAsync(entity);

                if (ObjectUtil.isNotEmpty(stopWatch)) {
                    stopWatch.stop();
                    log.info(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
                }
            });
    }

    private void buildGatewayLogAsync(GatewayLogDTO entity) {
        Schedulers.boundedElastic().schedule(() -> {
            long beginMillis = DatesUtil.parse(entity.getRequestBeginTime(), DatePattern.NORM_DATETIME_MS_PATTERN).getTime();
            long endMillis = System.currentTimeMillis();
            entity.setRequestEndTime(DatesUtil.parseDate(endMillis, DatePattern.NORM_DATETIME_MS_PATTERN));
            entity.setExecuteTime(endMillis - beginMillis);
            applicationContext.publishEvent(new GatewayLogEvent(this, entity));
        });
    }

    private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, GatewayLogDTO entity) {
        // 是否启用获取请求体
        final Boolean enabledRequestBody = this.property.getLog().getEnabledRequestBody();
        if (Boolean.FALSE.equals(enabledRequestBody)) {
            return null;
        }

        final ServerHttpRequest request = exchange.getRequest();

        Publisher<DataBuffer> bodyPublisher = request.getBody();

        final Integer limitRate = this.property.getLimitRate();
        final Integer limitBufferSize = this.property.getLimitDataBufferSize();
        final Flux<DataBuffer> flux = Flux.from(bodyPublisher)
            .limitRate(limitRate)
            .buffer(limitBufferSize * NumberConstant.NUMBER_ONE_THOUSAND_TWENTY_FOUR)
            .map(dataBuffers -> {
                byte[] content = readContent(dataBuffers);

                String requestBodyStr = new String(content, StandardCharsets.UTF_8);
                if (ObjectUtil.isNotEmpty(requestBodyStr)) {
                    Integer limitKb = property.getLog().getLimitRequestBody();
                    int limitBytes = limitKb != null ? limitKb * 1024 : Integer.MAX_VALUE;

                    final String str = CharSequenceUtil.maxLength(requestBodyStr, limitBytes)
                        .replaceAll(RegexConstant.WHITE_SPACE_REGEX, "");
                    entity.setRequestBody(str);
                }

                return dataBufferFactory.wrap(content);
            });

        return new ServerHttpRequestDecorator(request) {
            @Override
            public Flux<DataBuffer> getBody() {
                return flux;
            }
        };
    }

    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLogDTO entity) {
        // 是否启用返回响应结果
        final Boolean enabledResponseBody = this.property.getLog().getEnabledResponseBody();
        if (Boolean.FALSE.equals(enabledResponseBody)) {
            return null;
        }

        ServerHttpResponse response = exchange.getResponse();

        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                final Integer limitRate = property.getLimitRate();
                final Integer limitBufferSize = property.getLimitDataBufferSize();
                return super.writeWith(Flux.from(body)
                    .limitRate(limitRate)
                    .buffer(limitBufferSize * NumberConstant.NUMBER_ONE_THOUSAND_TWENTY_FOUR)
                    .map(dataBuffers -> {
                        byte[] content = readContent(dataBuffers);

                        String originalContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
                        boolean isJsonOrText = CharSequenceUtil.isNotBlank(originalContentType)
                            && (originalContentType.contains(MediaType.APPLICATION_JSON_VALUE)
                            || originalContentType.contains(MediaType.TEXT_PLAIN_VALUE));
                        if (isJsonOrText) {
                            String responseBodyStr = new String(content, StandardCharsets.UTF_8);
                            if (ObjectUtil.isNotEmpty(responseBodyStr)) {
                                Integer limitKb = property.getLog().getLimitResponseBody();
                                int limitBytes = limitKb != null ? limitKb * 1024 : Integer.MAX_VALUE;

                                final String str = CharSequenceUtil.maxLength(responseBodyStr, limitBytes)
                                    .replaceAll(RegexConstant.WHITE_SPACE_REGEX, "");
                                entity.setResponseBody(str);
                            }
                        }

                        return dataBufferFactory.wrap(content);
                    }));
            }
        };
    }

    private byte[] readContent(List<? extends DataBuffer> dataBuffers) {
        if (CollUtil.isEmpty(dataBuffers)) {
            return new byte[0];
        }

        DataBuffer join = this.dataBufferFactory.join(dataBuffers);
        byte[] content;
        try {
            content = new byte[join.readableByteCount()];
            join.read(content);
        } finally {
            DataBufferUtils.release(join);
        }
        return content;
    }

}
